/*
 Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights
 reserved.
 
 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License, version 2.0,
 as published by the Free Software Foundation.

 This program is also distributed with certain software (including
 but not limited to OpenSSL) that is licensed under separate terms,
 as designated in a particular file or component or in included license
 documentation.  The authors of MySQL hereby grant you an additional
 permission to link the program and your derivative works with the
 separately licensed software that they have included with MySQL.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License, version 2.0, for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 02110-1301  USA
 */

"use strict";

var        util    = require("util");
var     BitMask    = require(mynode.common.BitMask);
var      udebug    = unified_debug.getLogger("Query.js");
var userContext    = require("./UserContext.js");

var keywords = ['param', 'where', 'field', 'execute'];

var QueryParameter;
var QueryHandler;
var QueryEq, QueryNe, QueryLe, QueryLt, QueryGe, QueryGt, QueryBetween, QueryIn, QueryIsNull, QueryIsNotNull;
var QueryNot, QueryAnd, QueryOr;

/** QueryDomainType function param */
var param = function(name) {
  return new QueryParameter(this, name);
};

/** QueryDomainType function where */
var where = function(predicate) {
  var mynode = this.mynode_query_domain_type;
  mynode.predicate = predicate;
  mynode.queryHandler = new QueryHandler(mynode.dbTableHandler, predicate);
  mynode.queryType = mynode.queryHandler.queryType;
  this.prototype = {};
  return this;
};

/** QueryDomainType function execute */
var execute = function() {
  var session = this.mynode_query_domain_type.session;
  var context = new userContext.UserContext(arguments, 2, 2, session, session.sessionFactory);
  // delegate to context's execute for execution
  return context.executeQuery(this);
};

var queryDomainTypeFunctions = {};
queryDomainTypeFunctions.where = where;
queryDomainTypeFunctions.param = param;
queryDomainTypeFunctions.execute = execute;

/**
 * QueryField represents a mapped field in a domain object. QueryField is used to build
 * QueryPredicates by comparing the field to parameters.
 * @param queryDomainType
 * @param field
 * @return
 */
var QueryField = function(queryDomainType, field) {
  if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('QueryField<ctor>', field.fieldName);
//  this.class = 'QueryField'; // useful for debugging
//  this.fieldName = field.fieldName; // useful for debugging
  this.queryDomainType = queryDomainType;
  this.field = field;
};

QueryField.prototype.eq = function(queryParameter) {
  return new QueryEq(this, queryParameter);
};

QueryField.prototype.le = function(queryParameter) {
  return new QueryLe(this, queryParameter);
};

QueryField.prototype.ge = function(queryParameter) {
  return new QueryGe(this, queryParameter);
};

QueryField.prototype.lt = function(queryParameter) {
  return new QueryLt(this, queryParameter);
};

QueryField.prototype.gt = function(queryParameter) {
  return new QueryGt(this, queryParameter);
};

QueryField.prototype.ne = function(queryParameter) {
  return new QueryNe(this, queryParameter);
};

QueryField.prototype.between = function(queryParameter1, queryParameter2) {
  return new QueryBetween(this, queryParameter1, queryParameter2);
};

// 'in' is a keyword so use alternate syntax
QueryField.prototype['in'] = function(queryParameter) {
  return new QueryIn(this, queryParameter);
};

QueryField.prototype.isNull = function() {
  return new QueryIsNull(this);
};

QueryField.prototype.isNotNull = function() {
  return new QueryIsNotNull(this);
};

QueryField.prototype.inspect = function() {
  return this.field.fieldName;
};

/** Query Domain Type represents a domain object that can be used to create and execute queries.
 * It encapsulates the dbTableHandler (obtained from the domain object or table name),
 * the session (required to execute the query), and the filter which limits the result.
 * @param session the user Session
 * @param dbTableHandler the dbTableHandler
 * @param domainObject true if the query results are domain objects
 */
var QueryDomainType = function(session, dbTableHandler, domainObject) {
  udebug.log('QueryDomainType<ctor>', dbTableHandler.dbTable.name);
  // avoid most name conflicts: put all implementation artifacts into the property mynode_query_domain_type
  this.mynode_query_domain_type = {};
  this.field = {};
  var mynode = this.mynode_query_domain_type;
  mynode.session = session;
  mynode.dbTableHandler = dbTableHandler;
  mynode.domainObject = domainObject;
  var queryDomainType = this;
  // initialize the functions (may be overridden below if a field has the name of a keyword)
  queryDomainType.where = where;
  queryDomainType.param = param;
  queryDomainType.execute = execute;
  
  var fieldName, queryField;
  // add a property for each field in the table mapping
  mynode.dbTableHandler.fieldNumberToFieldMap.forEach(function(field) {
    fieldName = field.fieldName;
    queryField = new QueryField(queryDomainType, field);
    if (keywords.indexOf(fieldName) === -1) {
      // field name is not a keyword
      queryDomainType[fieldName] = queryField;
    } else {
      if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('QueryDomainType<ctor> field', fieldName, 'is a keyword.');
      // field name is a keyword
      // allow e.g. qdt.where.id
      if (fieldName !== 'field') {
        // if field is a reserved word but not a function, skip setting the function
        queryDomainType[fieldName] = queryDomainTypeFunctions[fieldName];
        queryDomainType[fieldName].eq = QueryField.prototype.eq;
        queryDomainType[fieldName].field = queryField.field;
      }
      // allow e.g. qdt.field.where
      queryDomainType.field[fieldName] = queryField;
    }
  });
};

QueryDomainType.prototype.inspect = function() { 
  var mynode = this.mynode_query_domain_type;
  return "[[API Query on table: " + mynode.dbTableHandler.dbTable.name + 
    ", type: " + mynode.queryType + ", predicate: " + 
    util.inspect(mynode.predicate) + "]]\n";
};

QueryDomainType.prototype.not = function(queryPredicate) {
  return new QueryNot(queryPredicate);
};

/**
 * QueryParameter represents a named parameter for a query. The QueryParameter marker is used
 * as the comparand for QueryField.
 * @param queryDomainType
 * @param name
 * @return
 */
QueryParameter = function(queryDomainType, name) {
  if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('QueryParameter<ctor>', name);
  this.queryDomainType = queryDomainType;
  this.name = name;
};

QueryParameter.prototype.inspect = function() {
  return '?' + this.name;
};

/******************************************************************************
 *                 SQL VISITOR
 *****************************************************************************/
var SQLVisitor = function(rootPredicateNode) {
  this.rootPredicateNode = rootPredicateNode;
  rootPredicateNode.sql = {};
  rootPredicateNode.sql.formalParameters = [];
  rootPredicateNode.sql.sqlText = 'initialized';
  this.parameterIndex = 0;
};

/** Handle nodes QueryEq, QueryNe, QueryLt, QueryLe, QueryGt, QueryGe */
SQLVisitor.prototype.visitQueryComparator = function(node) {
  // set up the sql text in the node
  var columnName = node.queryField.field.fieldName;
  node.sql.sqlText = columnName + node.comparator + '?';
  // assign ordered list of parameters to the top node
  this.rootPredicateNode.sql.formalParameters[this.parameterIndex++] = node.parameter;
};

/** Handle nodes QueryAnd, QueryOr */
SQLVisitor.prototype.visitQueryNaryPredicate = function(node) {
  var i;
  // all n-ary predicates have at least two
  node.predicates[0].visit(this); // sets up the sql.sqlText in the node
  node.sql.sqlText = '(' + node.predicates[0].sql.sqlText + ')';
  for (i = 1; i < node.predicates.length; ++i) {
    node.sql.sqlText += node.operator;
    node.predicates[i].visit(this);
    node.sql.sqlText += '(' + node.predicates[i].sql.sqlText + ')';
  }
};

/** Handle nodes QueryNot */
SQLVisitor.prototype.visitQueryUnaryPredicate = function(node) {
  node.predicates[0].visit(this); // sets up the sql.sqlText in the node
  node.sql.sqlText = node.operator + '(' + node.predicates[0].sql.sqlText + ')';
};

/** Handle nodes QueryIsNull, QueryIsNotNull */
SQLVisitor.prototype.visitQueryUnaryOperator = function(node) {
  var columnName = node.queryField.field.fieldName;
  node.sql.sqlText = columnName + node.operator;
};

/** Handle node QueryBetween */
SQLVisitor.prototype.visitQueryBetweenOperator = function(node) {
  var columnName = node.queryField.field.fieldName;
  node.sql.sqlText = columnName + ' BETWEEN ? AND ?';
  this.rootPredicateNode.sql.formalParameters[this.parameterIndex++] = node.formalParameters[0];
  this.rootPredicateNode.sql.formalParameters[this.parameterIndex++] = node.formalParameters[1];
};

/******************************************************************************
 *                 MARKS COLUMN MASKS IN QUERY NODES
 *****************************************************************************/
function MaskMarkerVisitor() {
}

/** Set column number in usedColumnMask */
function markUsed(node) {
  node.usedColumnMask = new BitMask(); 
  node.equalColumnMask = new BitMask();
  node.usedColumnMask.set(node.queryField.field.columnNumber);
}

/** Handle nodes QueryEq, QueryNe, QueryLt, QueryLe, QueryGt, QueryGe */
MaskMarkerVisitor.prototype.visitQueryComparator = function(node) {
  markUsed(node);
  if(node.operationCode === 4) {  // QueryEq
    node.equalColumnMask.set(node.queryField.field.columnNumber);
  }
};

/** Nodes Between, IsNotNull, IsNotNull are all handled by markUsed() */
MaskMarkerVisitor.prototype.visitQueryUnaryOperator = markUsed;
MaskMarkerVisitor.prototype.visitQueryBetweenOperator = markUsed;

/** Handle QueryNot */
MaskMarkerVisitor.prototype.visitQueryUnaryPredicate = function(node) {
  node.predicates[0].visit(this);
  node.equalColumnMask = new BitMask();  // Set to zero 
  node.usedColumnMask  = node.predicates[0].usedColumnMask;
};

/** Handle nodes QueryAnd, QueryOr */
MaskMarkerVisitor.prototype.visitQueryNaryPredicate = function(node) {
  var i;
  node.usedColumnMask = new BitMask(); 
  node.equalColumnMask = new BitMask();
  for(i = 0 ; i < node.predicates.length ; i++) {
    node.predicates[i].visit(this);
    node.usedColumnMask.orWith(node.predicates[i].usedColumnMask);
    if(this.operationCode === 1) {  // QueryAnd
      node.equalColumnMask.orWith(node.predicates[i].equalColumnMask);
    }
  }
};

var theMaskMarkerVisitor = new MaskMarkerVisitor();   // Singleton


/******************************************************************************
 *                 TOP LEVEL ABSTRACT QUERY PREDICATE
 *****************************************************************************/
var AbstractQueryPredicate = function() {
  this.sql = {};
};

AbstractQueryPredicate.prototype.inspect = function() {
  var str = this.operator + "(";
  this.predicates.forEach(function(value,index) { 
    if(index) str += " , ";
    str += value.inspect(); 
  });
  str += ")";
  return str;
};

AbstractQueryPredicate.prototype.and = function(predicate) {
  // TODO validate parameter
  return new QueryAnd(this, predicate);
};

AbstractQueryPredicate.prototype.andNot = function(predicate) {
  // TODO validate parameter
  return new QueryAnd(this, new QueryNot(predicate));
};

AbstractQueryPredicate.prototype.or = function(predicate) {
  // TODO validate parameter for OR
  return new QueryOr(this, predicate);
};

AbstractQueryPredicate.prototype.orNot = function(predicate) {
  // TODO validate parameter
  return new QueryOr(this, new QueryNot(predicate));
};

AbstractQueryPredicate.prototype.not = function() {
  // TODO validate parameter
  return new QueryNot(this);
};

AbstractQueryPredicate.prototype.getTopLevelPredicates = function() {
  return [this];
};

AbstractQueryPredicate.prototype.getSQL = function() {
  var visitor = new SQLVisitor(this);
  this.visit(visitor);
  return this.sql;
};

/******************************************************************************
 *                 ABSTRACT QUERY N-ARY PREDICATE
 *                          AND and OR
 *****************************************************************************/
var AbstractQueryNaryPredicate = function() {
};

AbstractQueryNaryPredicate.prototype = new AbstractQueryPredicate();

AbstractQueryNaryPredicate.prototype.getTopLevelPredicates = function() {
  return this.predicates;
};

AbstractQueryNaryPredicate.prototype.visit = function(visitor) {
  if (typeof(visitor.visitQueryNaryPredicate) === 'function') {
    visitor.visitQueryNaryPredicate(this);
  }
};

/******************************************************************************
 *                 ABSTRACT QUERY UNARY PREDICATE
 *                           NOT
 *****************************************************************************/
var AbstractQueryUnaryPredicate = function() {
};

AbstractQueryUnaryPredicate.prototype = new AbstractQueryPredicate();

AbstractQueryUnaryPredicate.prototype.visit = function(visitor) {
  if (typeof(visitor.visitQueryUnaryPredicate) === 'function') {
    visitor.visitQueryUnaryPredicate(this);
  }
};

/******************************************************************************
 *                 ABSTRACT QUERY COMPARATOR
 *                  eq, ne, gt, lt, ge, le
 *****************************************************************************/
var AbstractQueryComparator = function() {
};

/** AbstractQueryComparator inherits AbstractQueryPredicate */
AbstractQueryComparator.prototype = new AbstractQueryPredicate();

AbstractQueryComparator.prototype.inspect = function() {
  return this.queryField.field.fieldName + this.comparator + this.parameter.inspect();
};

AbstractQueryComparator.prototype.visit = function(visitor) {
  if (typeof(visitor.visitQueryComparator) === 'function') {
    visitor.visitQueryComparator(this);
  }
};

/******************************************************************************
 *                 QUERY EQUAL
 *****************************************************************************/
QueryEq = function(queryField, parameter) {
  this.comparator = ' = ';
  this.operationCode = 4;
  this.queryField = queryField;
  this.parameter = parameter;
};

QueryEq.prototype = new AbstractQueryComparator();

/******************************************************************************
 *                 QUERY LESS THAN OR EQUAL
 *****************************************************************************/
QueryLe = function(queryField, parameter) {
  this.comparator = ' <= ';
  this.operationCode = 0;
  this.queryField = queryField;
  this.parameter = parameter;
};

QueryLe.prototype = new AbstractQueryComparator();

/******************************************************************************
 *                 QUERY GREATER THAN OR EQUAL
 *****************************************************************************/
QueryGe = function(queryField, parameter) {
  this.comparator = ' >= ';
  this.operationCode = 2;
  this.queryField = queryField;
  this.parameter = parameter;
};

QueryGe.prototype = new AbstractQueryComparator();

/******************************************************************************
 *                 QUERY LESS THAN
 *****************************************************************************/
QueryLt = function(queryField, parameter) {
  this.comparator = ' < ';
  this.operationCode = 1;
  this.queryField = queryField;
  this.parameter = parameter;
};

QueryLt.prototype = new AbstractQueryComparator();

/******************************************************************************
 *                 QUERY GREATER THAN
 *****************************************************************************/
QueryGt = function(queryField, parameter) {
  this.comparator = ' > ';
  this.operationCode = 3;
  this.queryField = queryField;
  this.parameter = parameter;
};

QueryGt.prototype = new AbstractQueryComparator();

/******************************************************************************
 *                 QUERY BETWEEN
 *****************************************************************************/
QueryBetween = function(queryField, parameter1, parameter2) {
  this.comparator = ' BETWEEN ';
  this.queryField = queryField;
  this.formalParameters = [];
  this.formalParameters[0] = parameter1;
  this.formalParameters[1] = parameter2;
  this.parameter1 = parameter1;
  this.parameter2 = parameter2;
};

QueryBetween.prototype = new AbstractQueryComparator();

QueryBetween.prototype.inspect = function() {
  return this.queryField.inspect() + ' BETWEEN ' + this.parameter1.inspect() + 
    ' AND ' + this.parameter2.inspect();
};

QueryBetween.prototype.visit = function(visitor) {
  if (typeof(visitor.visitQueryBetweenOperator) === 'function') {
    visitor.visitQueryBetweenOperator(this);
  }
};

/******************************************************************************
 *                 QUERY NOT EQUAL
 *****************************************************************************/
QueryNe = function(queryField, parameter) {
  this.comparator = ' != ';
  this.operationCode = 5;
  this.queryField = queryField;
  this.parameter = parameter;
};

QueryNe.prototype = new AbstractQueryComparator();

/******************************************************************************
 *                 QUERY IN
 *****************************************************************************/
QueryIn = function(queryField, parameter) {
  this.comparator = ' IN ';
  this.queryField = queryField;
  this.parameter = parameter;
};

QueryIn.prototype = new AbstractQueryComparator();

/******************************************************************************
 *                 ABSTRACT QUERY UNARY OPERATOR
 *****************************************************************************/
var AbstractQueryUnaryOperator = function() {
};

AbstractQueryUnaryOperator.prototype = new AbstractQueryPredicate();

AbstractQueryUnaryOperator.prototype.inspect = function() {
  return util.format(this);
//  return this.queryField.inspect() + this.comparator + this.parameter.inspect();
};

AbstractQueryUnaryOperator.prototype.visit = function(visitor) {
  if (typeof(visitor.visitQueryUnaryOperator) === 'function') {
    visitor.visitQueryUnaryOperator(this);
  }
};

/******************************************************************************
 *                 QUERY IS NULL
 *****************************************************************************/
QueryIsNull = function(queryField) {
  this.operator = ' IS NULL';
  this.operationCode = 7;
  this.queryField = queryField;
};

QueryIsNull.prototype = new AbstractQueryUnaryOperator();

/******************************************************************************
 *                 QUERY IS NOT NULL
 *****************************************************************************/
QueryIsNotNull = function(queryField) {
  this.operator = ' IS NOT NULL';
  this.operationCode = 8;
  this.queryField = queryField;
};

QueryIsNotNull.prototype = new AbstractQueryUnaryOperator();

/******************************************************************************
 *                 QUERY AND
 *****************************************************************************/
QueryAnd = function(left, right) {
  this.operator = ' AND ';
  this.operationCode = 1;
  this.predicates = [left, right];
  if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('QueryAnd<ctor>', this);
};

QueryAnd.prototype = new AbstractQueryNaryPredicate();

QueryAnd.prototype.getTopLevelPredicates = function() {
  return this.predicates;
};

/** Override the "and" function to collect all predicates in one variable. */
QueryAnd.prototype.and = function(predicate) {
  this.predicates.push(predicate);
  return this;
};

/******************************************************************************
 *                 QUERY OR
 *****************************************************************************/
QueryOr = function(left, right) {
  this.operator = ' OR ';
  this.operationCode = 2;
  this.predicates = [left, right];
  if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('QueryOr<ctor>', this);
};

QueryOr.prototype = new AbstractQueryNaryPredicate();

QueryOr.prototype.getTopLevelPredicates = function() {
  return [];
};

/** Override the "or" function to collect all predicates in one variable. */
QueryOr.prototype.or = function(predicate) {
  this.predicates.push(predicate);
  return this;
};

/******************************************************************************
 *                 QUERY NOT
 *****************************************************************************/
QueryNot = function(left) {
  this.operator = ' NOT ';
  this.operationCode = 3;
  this.predicates = [left];
  if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('QueryNot<ctor>', this, 'parameter', left);
};

QueryNot.prototype = new AbstractQueryUnaryPredicate();


/******************************************************************************
 *                 CANDIDATE INDEX
 *****************************************************************************/
// CandidateIndex is almost stateless now.
// For future consideration: move mask into DbIndexHandler, then eliminate
// CandidateIndex completely.

var CandidateIndex = function(dbTableHandler, indexNumber) {
  this.dbIndexHandler = dbTableHandler.dbIndexHandlers[indexNumber];
  if(! this.dbIndexHandler) {
    console.log("indexNumber", typeof(indexNumber));
    console.trace("not an index handler");
    throw new Error('Query.CandidateIndex<ctor> indexNumber is not found');
  }
  this.isOrdered = this.dbIndexHandler.dbIndex.isOrdered;
  this.isUnique = this.dbIndexHandler.dbIndex.isUnique;
  if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('CandidateIndex<ctor> for index', this.dbIndexHandler.dbIndex.name,
      'isUnique', this.isUnique, 'isOrdered', this.isOrdered);
  this.indexColumns = this.dbIndexHandler.dbIndex.columnNumbers;
  var mask = new BitMask(dbTableHandler.dbTable.columns.length);
  this.indexColumns.forEach(function(columnNumber) {
    mask.set(columnNumber);
  });
  this.mask = mask;
};

CandidateIndex.prototype.isUsable = function(predicate) {
  var usable;
  if (this.isUnique) {
    usable = predicate.equalColumnMask.and(this.mask).isEqualTo(this.mask);
  } else if(this.isOrdered) {
    usable = predicate.usedColumnMask.bitIsSet(this.indexColumns[0]);
  }
  return usable;
};


// This is used in Primary Key & Unique Key queries.
// param predicate: query predicate
// param parameterValues: the parameters object passed to query.execute()
// It returns an array, in key-column order, of the key values from the 
// parameter object.
CandidateIndex.prototype.getKeys = function(predicate, parameterValues) {

  function getParameterNameForColumn(node, columnNumber) {
    var i, name;
    if(node.equalColumnMask.bitIsSet(columnNumber)) {
      if(node.queryField && node.queryField.field.columnNumber == columnNumber) {
        return node.parameter.name;
      }
      if(node.predicates) {
        for(i = 0 ; i < node.predicates.length ; i++) {
          name = getParameterNameForColumn(node, columnNumber);
          if(name !== null) return name;
        }
      }
    }
    return null;
  }

  var result = [];
  var candidateIndex = this;
  this.indexColumns.forEach(function(columnNumber) {
    result.push(parameterValues[getParameterNameForColumn(predicate, columnNumber)]);
  });
  if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('CandidateIndex.getKeys parameters:', parameterValues,
                    'key:', result);
  return result;
};

/** Evaluate candidate indexes.
    Score 1 point for each consecutive key part used plus 1 more point
    if the column is in QueryEq. */
CandidateIndex.prototype.score = function(predicate) {
  var score = 0;
  var point, i;
  i = 0;
  do {
    point = predicate.usedColumnMask.bitIsSet(this.indexColumns[i]);
    if(point) { 
      score += 1; 
      if(predicate.usedColumnMask.bitIsSet(this.indexColumns[i])) {
        score += 1;
      }
    }
    i++;
  } while(point && i < this.indexColumns.length);
    
  if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('score', this.dbIndexHandler.dbIndex.name, 'is', score);
  return score;
};

/******************************************************************************
 *                 QUERY HANDLER
 *****************************************************************************/
/* QueryHandler constructor
 * IMMEDIATE
 * 
 * statically analyze the predicate to decide whether:
 * all primary key fields are specified ==> use primary key lookup;
 * all unique key fields are specified ==> use unique key lookup;
 * some (leading) index fields are specified ==> use index scan;
 * none of the above ==> use table scan
 * Get the query handler for a given query predicate.
 * 
 */
var QueryHandler = function(dbTableHandler, predicate) {
  if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('QueryHandler<ctor>', util.inspect(predicate));
  this.dbTableHandler = dbTableHandler;
  this.predicate = predicate;
  var indexes = dbTableHandler.dbTable.indexes;

  // Mark the usedColumnMask and equalColumnMask in each query node
  predicate.visit(theMaskMarkerVisitor);
  
  // create a CandidateIndex object for each index
  // if the primary index is usable, choose it
  var primaryCandidateIndex = new CandidateIndex(dbTableHandler, 0);

  if(primaryCandidateIndex.isUsable(predicate)) {
    // we're done!
    this.candidateIndex = primaryCandidateIndex;
    this.queryType = 0; // primary key lookup
    return;
  }
  // otherwise, look for a usable unique index
  var uniqueCandidateIndex, orderedCandidateIndexes = [];
  var i, index;
  for (i = 1; i < indexes.length; ++i) {
    index = indexes[i];
    if (index.isUnique) {
      // create a candidate index for unique index
      uniqueCandidateIndex = new CandidateIndex(dbTableHandler, i);
      if (uniqueCandidateIndex.isUsable(predicate)) {
        this.candidateIndex = uniqueCandidateIndex;
        this.queryType = 1; // unique key lookup
        // we're done!
        return;
      }
    } else if (index.isOrdered) {
      // create an array of candidate indexes for ordered indexes to be evaluated later
      orderedCandidateIndexes.push(new CandidateIndex(dbTableHandler, i));
    } else {
      throw new Error('FatalInternalException: index is not unique or ordered... so what is it?');
    }
  }
  // otherwise, look for the best ordered index (largest number of usable query terms)
  // choose the index with the biggest score
  var topScore = 0, candidateScore = 0;
  var bestCandidateIndex = null;
  orderedCandidateIndexes.forEach(function(candidateIndex) {
    candidateScore = candidateIndex.score(predicate);
    if (candidateScore > topScore) {
      topScore = candidateScore;
      bestCandidateIndex = candidateIndex;
    }
  });
  udebug.log("Best score is", topScore);
  if (topScore > 0) {
    this.candidateIndex = bestCandidateIndex;
    this.dbIndexHandler = bestCandidateIndex.dbIndexHandler;
    this.queryType = 2; // index scan
  } else {
    this.queryType = 3; // table scan
  }
};

/** Get key values from candidate indexes and parameters */
QueryHandler.prototype.getKeys = function(parameterValues) {
  return this.candidateIndex.getKeys(this.predicate, parameterValues);
};

exports.QueryDomainType = QueryDomainType;
exports.QueryHandler = QueryHandler;
